////////////////////////////////////////////////////////////////////////////////
//
//	This file is part of Woozox, see <http://code.google.com/p/woozox/>.
//
//	Copyright 2008-2009 Florian David
//
//	Woozoo is free software: you can redistribute it and/or modify
//	it under the terms of the GNU Lesser General Public License as 
//	published by the Free Software Foundation, either version 3 of 
//	the License, or (at your option) any later version.
//
//	Woozoo is distributed in the hope that it will be useful,
//	but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//	GNU General Public License for more details.
//
//	You should have received a copy of the GNU Lesser General Public 
//	License along with Woozoo. If not, see <http://www.gnu.org/licenses/>.
//
////////////////////////////////////////////////////////////////////////////////
package org.woozoo.xhtml.core
{
	import flash.events.*;
	
	import org.woozoo.xhtml.core.base.*;
	import org.woozoo.xhtml.core.base.error.ErrorReason;
	import org.woozoo.xhtml.core.base.event.DocumentEvent;
	import org.woozoo.xhtml.core.manager.cache.CacheManager;
	import org.woozoo.xhtml.core.manager.document.DocumentManager;
	import org.woozoo.xhtml.core.manager.element.ElementManager;
	import org.woozoo.xhtml.core.util.*;


	/**
	 * The API class of Woozox. It is a singleton.
	 * 
	 * <p>Most of the time you will define a public property to the classes that are using this class :<br />
	 * <code>public var api : API = API.getInstance();</code>,<br />
	 * then you will be able to call method of the api like<br />
	 * <code>api.setup()</code>.</p>
	 * 
	 * <p>To use the API, you will have to setup it using the setup method</p>
	 * 
	 * <code>
	 * public var api : API = API.getInstance();<br/>
	 * <br/>
	 * public function init() : void {<br/>
	 * 	 	api.setup('http://myWebsite.com', ready);<br/>
	 * }<br/>
	 * <br/>
	 * public function ready(document : Document) : void {<br/>
	 * 		// do stuff<br/>
	 * }<br/>
	 * </code>
	 * 
	 * @see #setup()
	 * */
	[Event(name="complete", type="org.woozoo.xhtml.core.base.event.DocumentEvent")]
	[Event(name="error", type="org.woozoo.xhtml.core.base.event.DocumentEvent")]
	public class API extends EventDispatcher
	{
		public var useEvent : Boolean = false;
		
		/**
		 * @private
		 * */
		protected static var _instanciable : Boolean = false;
		
		private static var __instance : API;
		
		/**
		 * Right before a document is loaded, this method will be called.
		 * 
		 * <p>You could for example show a loader on you page (in flex, something like : CursorManager.setBusyCursor())</p>
		 * */
		public var startLoadingCallBack : Function;

		/**
		 * Right after a document is loaded, this method will be called.
		 * 
		 * <p>You could for example show a loader on you page (in flex, something like : CursorManager.removeBusyCursor())</p>
		 * */
		public var stopLoadingCallBack : Function;
		
		/**
		 * @private
		 * Proxy access to the documentManager
		 * */
		protected var _documentManager : DocumentManager;
		
		/**
		 * @private
		 * Proxy access to the documentManager
		 * */
		protected var _cacheManager : CacheManager;
		
		/**
		 * 
		 * */
		private var __documentCallBacks : Object = {};
		
		/**
		 * Singleton getter
		 * 
		 * @return The singleton
		 * */
		 public static function getInstance() : API {
			if (!__instance) {
				_instanciable = true;
				__instance = new API();
				_instanciable = false;
			}
			return __instance;
		}
		
		/**
		 * @private
		 * Constructor
		 * */
		public function API() {
			_construct();
		}
		
		/**
		 * @private
		 * Constructor
		 * */
		protected function _construct() : void {
			if (!_instanciable) {
				throw new Error('API is a singleton class, call API.getInstance() instead');
			}
			__createDocumentManagerIfNecessary();	
			__createCacheManagerIfNecessary();	
		}
		
		/**
		 * Used to setup your woozox application. This is the fist method you have to use before using anything else in the API.
		 * 
		 * <p>The setup method use in the background the API.loadDocument method, so take attention to your successCallBack and errorCallBack.</p>
		 * 
		 * @see #loadDocument()
		 * 
		 * @param indexUrl The url of you index document (such as http://blog.mywebsite.com/)
		 * @param successCallBack If woozox succeed to load you index document, this callback will be called
		 * @param errorCallBack If woozox failed to load you index document, this callback will be called
		 * @param indexId Each document in woozox has an id, this is the id of the index document
		 * */
		public function setup(indexUrl : String, successCallBack : Function = null, errorCallBack : Function = null, indexId : String = "index") : void {
			var document : Document = _documentManager.setup(indexUrl, indexId);
			loadDocument(document, successCallBack, errorCallBack);
		}

		
		/**
		 * Get the index document, such as defined in the setup method
		 * 
		 * @return The index document
		 * */
		public function getDocumentIndex() : Document {
			return _documentManager.documentIndex;
		}
		
		/**
		 * Get the document that has the name "name"
		 * 
		 * @param name The name of the document
		 * @return The document
		 * */
		public function getDocumentByName(name : String) : Document {
			return _documentManager.getDocumentByName(name);
		}

		/**
		 * Return the list of document that are under the category "categoryName"
		 * 
		 * @param name The categoryName of the category
		 * @return The list of Document that match
		 * */
		public function getDocumentsByCategoryName(categoryName : String) : Array /* of Document*/ {
			return _documentManager.getDocumentsByCategoryName(categoryName);
		}
		
		/**
		 * Will return the list of registered Category.
		 * 
		 * @return The list
		 * */
		public function get categories() : Array {
			return _documentManager.categories;
		}
		
		public function addCategoryToDocuments(category : Category, list : Array) : void {
			var len : int = list.length;
			for (var i : int = 0; i < len; i++) {
				var document : Document = list[i];
				document.addCategory(category);
				category.addDocument(document);
			}
		}
		
		/**
		 * Return the Category with the specified name.
		 * 
		 * @return The category 
		 * */
		public function getCategoryByName(name : String) : Category {
			return _documentManager.getCategoryByName(name);
		}
		
		/**
		 * Will create a document with the specified id, url and category.
		 * 
		 * <p>Later you will be able to load this document with loadDocument().</p>
		 * 
		 * @param id The id of your document
		 * @param url The url of your document
		 * @param category The category you wish to add your document
		 * @return The newly created document
		 * */
		public function addAndCreateDocument(id : String, url : String, category : Category = null) : Document {
			return _documentManager.addAndCreateDocument(id, url, [category]);
		}
		
		/**
		 * Load the specified document and call the successCallBack if it succeed and errorCallBack if not.
		 * <p>Loading a document will permit you to access his XHTML. 
		 * That means you will be able to get all the informations in the page with the methods $ or $$.</p>
		 * 
		 * <p>The successCallBack method signature has to be :<br />
		 * <code>function successCallBack(document : Document)</code>.
		 * </p>
		 * 
		 * <p>The errorCallBack method signature has to be :<br />
		 * <code>function errorCallBack(reason : DocumentErrorReason, document : Document)</code>.
		 * </p>
		 * 
		 * @see org.woozoo.xhtml.core.base.DocumentErrorReason
		 * 
		 * @param document The document to load
		 * @param successCallBack The method to call in case of success
		 * @param errorCallBack The method to call in case of failure
		 * @return The document you pass as the first parameter
		 * */
		public function loadDocument(document : Document, successCallBack : Function = null, errorCallBack : Function = null) : Document {
			document.addEventListener(Event.COMPLETE, __loadDocumentCompleteHandler);
			document.addEventListener(IOErrorEvent.IO_ERROR, __loadDocumentErrorHandler);
			document.addEventListener(SecurityErrorEvent.SECURITY_ERROR, __loadDocumentErrorHandler);
			
			__documentCallBacks[document.name] = {'success': successCallBack, 'error':errorCallBack};
			
			__callStartLoadingCallBack();
			return _documentManager.loadDocument(document);
		}
		
		private function __loadDocumentRemoveListeners(document : Document) : void {
			document.removeEventListener(Event.COMPLETE, __loadDocumentCompleteHandler);
			document.removeEventListener(IOErrorEvent.IO_ERROR, __loadDocumentErrorHandler);
			document.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, __loadDocumentErrorHandler);
		}
		
		private function __loadDocumentCompleteHandler(e : Event) : void {
			var document : Document = e.currentTarget as Document;
			var documentCallBacks : Object = __documentCallBacks[document.name];
			var successCallBack : Function = documentCallBacks.success;
			var errorCallBack : Function = documentCallBacks.error;
			
			__loadDocumentRemoveListeners(document);
			_loadDocumentComplete(document, successCallBack, errorCallBack);
		}
		
		protected function _loadDocumentComplete(document : Document, successCallBack : Function, errorCallBack : Function) : void {
			__callStopLoadingCallBack();
			if (!document.isValid) {
				var errorReason : DocumentErrorReason = new DocumentErrorReason(DocumentErrorReason.NOT_XHTML);
				if (errorCallBack != null) {
					errorCallBack(errorReason, document);
				}
				if (useEvent) {
					dispatchEvent(new DocumentEvent(DocumentEvent.ERROR, document, errorReason));
				}
			} else {
				if (successCallBack != null) {
					successCallBack(document);
				}
				if (useEvent) {
					dispatchEvent(new DocumentEvent(DocumentEvent.COMPLETE, document));
				}
			}
		}
		
		private function __loadDocumentErrorHandler(e : Event) : void {
			var document : Document = e.currentTarget as Document;
			var documentCallBacks : Object = __documentCallBacks[document.name];
			var successCallBack : Function = documentCallBacks.success;
			var errorCallBack : Function = documentCallBacks.error;
			
			__loadDocumentRemoveListeners(document);
			var data : String = (e.type == IOErrorEvent.IO_ERROR) ? ErrorReason.IO_ERROR : ErrorReason.SECURITY_ERROR;
			__callStopLoadingCallBack();
			var errorReason : DocumentErrorReason = new DocumentErrorReason(data);
			if (errorCallBack != null) {
				errorCallBack(errorReason, document);
			}
			if (useEvent) {
				dispatchEvent(new DocumentEvent(DocumentEvent.ERROR, document, errorReason));
			}
		}
		
		private function __callStartLoadingCallBack() : void {
			if (startLoadingCallBack != null) {
				startLoadingCallBack();
			}
		}
		private function __callStopLoadingCallBack() : void {
			if (stopLoadingCallBack != null) {
				stopLoadingCallBack();
			}
		}
		
		/**
		 * Same as loadDocument() except you pass the id of the document instead of the document itself
		 * 
		 * @see #loadDocument()
		 * 
		 * @param document The id of the document to load
		 * @param successCallBack The method to call in case of success
		 * @param errorCallBack The method to call in case of failure
		 * @return The document that match the id you pass as the first parameter
		 * */
		public function loadDocumentByName(name : String, sucessCallBack : Function, errorCallBack : Function = null) : Document {
			return loadDocument(getDocumentByName(name), sucessCallBack, errorCallBack);
		}
		
		/**
		 * Add the specified document to list of available document
		 * 
		 * @param document The document
		 * @return The document you pass in the first parameter
		 * */
		public function addDocument(document : Document) : Document {
			return _documentManager.addDocument(document);
		}
		
		/**
		 * Similar to addAndCreateDocumentsByContainer() except we found the element with the id containerId
		 * 
		 * @see #addAndCreateDocumentsByContainer()
		 * 
		 * @param document The document you want to find documents
		 * @param containerId The id of the element that contains the list of documents
		 * @param category The category you wish to add the documents in
		 * @return The list of newly created document
		 * */
		public function addAndCreateDocumentsByContainerId(document : Document, containerId : String, category : Category = null) : Array /* of Document */ {
			return addAndCreateDocumentsByContainer(document, getElementById(document, containerId), category);
		}

		/**
		 * Will permit you to add easily available documents.
		 * 
		 * <p>This method will look for all 'a' tags in the container 
		 * and add documents by using addAndCreateDocument(id, url)<br/> 
		 * - the 'innerHTML' (stripTagged) of the 'a' for 'id'<br/> 
		 * - the 'href' tag of the 'a' for 'url'</p>
		 * 
		 * @see #addAndCreateDocument()
		 * 
		 * @param document The document you want to find documents
		 * @param container The element that contains the list of documents
		 * @param category The category you wish to add the documents in
		 * @return The list of newly created document
		 * */
		public function addAndCreateDocumentsByContainer(document : Document, container : Element, category : Category = null) : Array /* of Document */ {
			return _documentManager.addAndCreateDocumentsByContainer(document, container, [category]);
		}
		
		/**
		 * Equivalent of the javascript method document.getElementById.
		 * 
		 * @see #$()
		 * @see #getSelectedDocumentElementById()
		 * 
		 * @param document The document to search the id on
		 * @param element id The id of the element to look for
		 * @return The element that match  or null if nothing found
		 * */
		public function getElementById(document : Document, elementId : String) : Element {
			return document.getElementById(elementId);
		}
		
		/**
		 * Equivalent to the javascript method document.getElementsByTagName().
		 * 
		 * @see org.woozoo.xhtml.core.base.Element#getElementsByTagName()
		 * 
		 * @param document The document you wish to parse
		 * @param tagName The tag you are looking for
		 * @param maxDepth How deep in children you want to go (-1 is infinite)
		 * @param maxNumber Number of max element you are looking for (-1 is infinite)
		 * @return the list of Elements matching
		 * */
		public function getElementsByTagName(document : Document, tagName : String, maxDepth : int = -1) : Array /* of Element */ {
			return document.getElementsByTagName(tagName, maxDepth);
		}
		
		/**
		 * Equivalent to the javascript method document.getElementsByClassName().
		 * 
		 * @see org.woozoo.xhtml.core.base.Element#getElementsByClassName()
		 * 
		 * @param document The document you wish to parse
		 * @param className The tag you are looking for
		 * @param maxDepth How deep in children you want to go (-1 is infinite)
		 * @param maxNumber Number of max element you are looking for (-1 is infinite)
		 * @return the list of Elements matching
		 * */
		public function getElementsByClassName(document : Document, className : String, maxDepth : int = -1) : Array /* of Element */ {
			return document.getElementsByClassName(className, maxDepth);
		}
		
		/**
		 * Equivalent of the javascript method document.getElementById.
		 * 
		 * <p>This method is a proxy to getElementById.</p>
		 * <p>$('myId') will return the element with id myId</p>
		 * 
		 * @see #getElementById()
		 * 
		 * @param document The document
		 * @param elementId The id of the element you are looking for
		 * @return The matching element or null if nothing found
		 * */
		public function $(document : Document, elementId : String) : Element {
			return document.$(elementId);
		}
		
		/**
		 * Equivalent of the javascript method querySelectorAll or querySelector (CSS selector).
		 * 
		 * <p>The special char supported are ' ', '.', '>', '[]' and '[=""]'</p>
		 * <p>
		 * $$('div.storycontent') will return all the divs with the class storycontent<br />
		 * $$('div') will return all the divs<br />
		 * $$('#myId') will return an array containings the element with id myId<br />
		 * $$('.storycontent')will return all the elements with the class storycontent<br />
		 * $$('#myId>div') <br />
		 * $$('div[rel="category"]')<br />
		 * $$('div[title]')
		 * </p>
		 * 
		 * @see #$()
		 * @see #querySelectorAll()
		 * @see #querySelector()
		 * @see http://www.w3.org/TR/css3-selectors/#selectors
		 * 
		 * @param document The document
		 * @param selectors The CSS selector
		 * @param selectAll Use true to use querySelectorAll, false to use querySelector
		 * @return The list of matching Element if selectAll is set to true, the first element matching if selectAll is set to false
		 * */
		public function $$(document : Document, selectors : String, selectAll : Boolean = true) : * /* Array of Element or Element */ {
			return document.$$(selectors, selectAll);
		}
		
		/**
		 * Returns a list of the elements within the document (using depth-first pre-order traversal of the document's nodes) that match the specified group of selectors.
		 * 
		 * @param document The document
		 * @param selectors A string containing one or more CSS selectors separated by commas.
		 * @return The list of matching element
		 * 
		 * @see http://www.w3.org/TR/css3-selectors/#selectors
 		 * @see http://developer.mozilla.org/en/DOM/document.querySelectorAll
		 * */
		public function querySelectorAll(document : Document, selectors : String) : Array {
			return document.querySelectorAll(selectors);
		}

		/**
		 * Returns the first element within the document (using depth-first pre-order traversal of the document's nodes) that matches the specified group of selectors.
		 * 
		 * @param document The document
		 * @param selectors A string containing one or more CSS selectors separated by commas.
		 * @return The first element matching
		 * 
		 * @see http://developer.mozilla.org/En/DOM/Document.querySelector
		 * */
		public function querySelector(document : Document, selectors : String) : Element {
			return document.querySelector(selectors);
		}
		
		/**
	     * This method strips the html tags from a String and returns the results.
	     * The second param allows for certain tags not to be stripped from the string
	     * and are formatted as a String: "&lt;p&gt;&lt;b&gt;&lt;br /&gt;"
	     *
	     * @param str The String or Element to strip it's tags
	     * @param sllowedTags A string of tags allowed
	     * @return A new string with the tags stripped
		 * */
		public function stripTags(str : *, allowedTags : String = null) : String {
			return StringUtil.stripTags(str, allowedTags);
		} 
		
		/**
		 * This method remove whitespace at the beginning ant the end of your string
		 * 
		 * @param str The string or Element to strip
		 * @return The string stripped
		 * */
		public function strip(str : *) : String {
			return StringUtil.strip(str);
		} 
		
		/**
		 * This methos remove whitespace that are in a string.
		 * 
		 * <p>The second param permit you to remove only certain type of whitespace (you can combine by concataining more than one char) :<br/>
		 * - use 's' to remove space,<br/>
		 * - use 't' to remove tab,<br />
		 * - use 'r' to remove \r, <br />
		 * - use 'n' to remove \n.<br />
		 * The default value is to the empty string and is equivalent to 'strn'</p>
		 * 
		 * @param str The string or Element to strip
		 * @param options The options to apply
		 * @return The string stripped
		 * */
		public function stripWhitespace(str : *, options : String = "") : String {
			return StringUtil.stripWhitespace(str, options);
		}
	
		/**
		 * Return an array of value (contained in attributeName) that are on every Element of the list.
		 * 
		 * <p>For example, with list = ['&lt;div title="myTitle" id="myId"/&gt;', '&lt;div title="myTitle3" id="myId3"/&gt;'],<br />
		 * getAttributeOnElements(list, 'title') will return <br />
		 * ['myTitle', 'myTitle3']</p>
		 * 
		 * @param list The list of Element
		 * @param attributeName The name of the attribute
		 * 
		 * @return The list
		 * */
		public function getAttributeOnElements(list : Array, attributeName : String) : Array {
			return ElementArrayUtil.getAttribute(list, attributeName);
		}
	
		/**
		 * Similar to getAttributeOnElements but with more than one attibute.
		 * 
		 * <p>For example, with list = [&lt;div title="myTitle" id="myId"/&gt;,&lt;div title="myTitle3" id="myId3"/&gt;],<br />
		 * getAttributesOnElements(list, 'title', 'id') will return <br />
		 * [{title:'myTitle', id:'myId'}, {title:'myTitle3', id:'myId3'}]</p>
		 * 
		 * @see #getAttributeOnElements()
		 * 
		 * @param list The list of Element
		 * @param args A list of name of attributes
		 * @return The list
		 * */
		public function getAttributesOnElements(list : Array, ...args) : Array /* of Object */ {
			return ElementArrayUtil.getAttributes(list, args);
		}
		
		/**
		 * The class that will be used to create document. 
		 * 
		 * <p>Set this value before calling the setup method because this method will use it to create your index document.</p>
		 * 
		 * @default org.woozoo.xhtml.core.base.Document
		 * @see #setup()
		 * */
		public function get defaultDocumentClass() : Class {
			__createDocumentManagerIfNecessary();
			return _documentManager.defaultDocumentClass;
		}
		public function set defaultDocumentClass(value : Class) : void {
			__createDocumentManagerIfNecessary();
			_documentManager.defaultDocumentClass = value;
		}
		
		/**
		 * The class that will be used to create category. 
		 * 
		 * <p>Set this value before calling the setup method because this method will use it to create your categories.</p>
		 * 
		 * @default org.woozoo.xhtml.core.base.Category
		 * @see #setup()
		 * */
		public function get defaultCategoryClass() : Class {
			__createDocumentManagerIfNecessary();
			return _documentManager.defaultCategoryClass;
		}
		public function set defaultCategoryClass(value : Class) : void {
			__createDocumentManagerIfNecessary();
			_documentManager.defaultCategoryClass = value;
		}
		
		/**
		 * Add class to create elements by tagName. 
		 * 
		 * <p>Set this values before calling the setup method.</p>
		 * <p>All the class should extends the base class Element.</p>
		 * <p>If you wish that all p elements has the class PElement, create a class called PElement extends Element,
		 * then add it using addElementClassByTagName('p', PElement)</p>
		 * 
		 * @see #setup()
		 * */
		public function addElementClassByTagName(tagName : String, classRef : Class) : void {
			return ElementManager.getInstance().addClass(tagName, classRef);
		}

		/**
		 * Enable cache for selector. 
		 * 
		 * <p>If you call twice the same selector using for example $$ or querySelectorAll, the elements returned are cached for later call.</p>
		 * */
		public function get enableSelectorCache() : Boolean {
			__createCacheManagerIfNecessary();
			return CacheManager.getInstance().enabled;
		}
		public function set enableSelectorCache(value : Boolean) : void {
			__createCacheManagerIfNecessary();
			CacheManager.getInstance().enabled = value;
		}
		
		private function __createCacheManagerIfNecessary() : void {
			if (!_cacheManager) {
				_cacheManager = CacheManager.getInstance();
			}
		}
		private function __createDocumentManagerIfNecessary() : void {
			if (!_documentManager) {
				_documentManager = DocumentManager.getInstance();
			}	
		}
	}
}